iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
自我挑戰組

用數據守護雙眼:生活型態對視力影響的探索性資料分析系列 第 27 篇

強強聯手!軟投票集成與分類門檻移動

  • 分享至 

  • xImage
  •  

針對 Day 26 案例 B 揭示的「邊界樣本因特徵正負拉扯、在固定 0.33 均分門檻下保守退化為中度風險」的痛點,結合演算法融合與決策後處理技術。首先將表現最優異的 LightGBM(冠軍) 與穩定度最佳的 隨機森林優化版(亞軍) 透過軟投票集成進行預測機率平滑融合;接著實施針對「高度疲勞風險」的動態分類門檻移動,全面壓榨模型在高風險篩檢上的邊際效能。

一、實作前情境:為什麼需要軟投票與門檻移動?
在經過 Day 19 至 Day 26 的系列實驗後,我們獲得了兩項關鍵認知:

單一模型的決策盲區:

  • 隨機森林優化版:具備最高的排序穩定度(Macro ROC-AUC = 0.553),但樹深限制使其在高風險的機率輸出較為保守。

  • LightGBM:擁有最強的三類別平衡度(Macro F1 = 0.417)與高風險召回(11 筆),但單一葉節點生長容易受特定自覺評分雜訊干擾(如案例 B 呈現的拉扯)。

2.預設 Argmax 門檻(1/3)的臨床局限:在三分類任務中,predict()

  • 預設將樣本判給「機率最大」的類別。當低、中、高機率分別為 [0.25, 0.38, 0.37] 時,模型會判為中度風險(Medium),導致像案例 B 這樣真實為高風險(23.74% 機率)的樣本被漏報。
  • 門檻移動:在臨床預防醫學中,漏報高風險的代價遠高於誤報。只要模型評估高風險的後驗機率超過動態門檻 tau_High(例如 0.28~0.30),即主動觸發高風險警示,以極小的誤報代價換取最大化的安全攔截率。

二、撰寫軟投票集成與動態門檻調優腳本
在 Colab 中新增儲存格,使用 scikit-learn 建構 VotingClassifier 並搜尋最佳高風險判定門檻:

# ==========================================
# Day 27:軟投票集成與高風險動態門檻
# ==========================================

import warnings
warnings.filterwarnings('ignore')

import matplotlib.pyplot as plt
import seaborn as sns
import matplotlib.font_manager as fm
import pandas as pd
import numpy as np

from sklearn.ensemble import VotingClassifier
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score, f1_score, recall_score, precision_score

# 1. 引用中文字型
font_path = 'NotoSansTC-Regular.otf'
my_font = fm.FontProperties(fname=font_path)

# 2. 構建軟投票集成模型 (Soft Voting Ensemble)
# 結合第一名 LightGBM (權重 0.6) 與第二名 隨機森林 (權重 0.4)
ensemble_clf = VotingClassifier(
    estimators=[
        ('lgb', lgb_clf),
        ('rf', best_rf)
    ],
    voting='soft',
    weights=[0.6, 0.4]
)

ensemble_clf.fit(X_train, y_train)

# 3. 取得集成測試集預測機率矩陣
y_prob_ens = ensemble_clf.predict_proba(X_test)
y_pred_default = ensemble_clf.predict(X_test)

# 4. 尋找高風險 (Class 2: High) 最佳決策門檻 (Threshold Moving)
thresholds = np.linspace(0.15, 0.55, 81)
eval_records = []

for th in thresholds:
    preds = []
    for p in y_prob_ens:
        if p[2] >= th:
            preds.append(2)
        else:
            preds.append(np.argmax(p[:2]))  # 在 Low(0) 與 Medium(1) 間競逐
            
    preds = np.array(preds)
    
    rec_high = recall_score(y_test == 2, preds == 2, zero_division=0)
    prec_high = precision_score(y_test == 2, preds == 2, zero_division=0)
    f1_high = f1_score(y_test == 2, preds == 2, zero_division=0)
    macro_f1 = f1_score(y_test, preds, average='macro', zero_division=0)
    
    eval_records.append({
        'threshold': th,
        'recall_high': rec_high,
        'prec_high': prec_high,
        'f1_high': f1_high,
        'macro_f1': macro_f1
    })

eval_df = pd.DataFrame(eval_records)

# 挑選使整體 Macro F1 與高風險 F1 最佳平衡的門檻值
best_row = eval_df.sort_values(by=['macro_f1', 'f1_high'], ascending=False).iloc[0]
best_th = best_row['threshold']
print(f"=== 搜尋最佳決策門檻完成 ===")
print(f"• 推薦高風險判定門檻: {best_th:.3f}")
print(f"• 預估高風險召回率: {best_row['recall_high']*100:.1f}%")
print(f"• 預估整體 Macro F1-score: {best_row['macro_f1']:.4f}\n")

# 5. 繪製 1x2 畫布:門檻權衡曲線 + 優化後混淆矩陣
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
sns.set_theme(style="whitegrid")

# --- 圖 1:動態門檻與高風險評估指標變化曲線 ---
axes[0].plot(eval_df['threshold'], eval_df['recall_high'], label='高風險召回率 (High Recall)', color='#e74c3c', linewidth=2.5)
axes[0].plot(eval_df['threshold'], eval_df['prec_high'], label='高風險精確率 (High Precision)', color='#2980b9', linewidth=2)
axes[0].plot(eval_df['threshold'], eval_df['f1_high'], label='高風險 F1-score', color='#8e44ad', linewidth=2, linestyle='-.')
axes[0].plot(eval_df['threshold'], eval_df['macro_f1'], label='全域 Macro F1-score', color='#27ae60', linewidth=2.5, linestyle='--')

axes[0].axvline(best_th, color='black', linestyle=':', linewidth=1.5, label=f'最佳門檻 ({best_th:.2f})')
axes[0].set_title('高度疲勞風險決策門檻調優權衡曲線', fontproperties=my_font, fontsize=13, fontweight='bold')
axes[0].set_xlabel('高度風險判定門檻 (Threshold)', fontproperties=my_font, fontsize=12)
axes[0].set_ylabel('指標得分 (0.0 ~ 1.0)', fontproperties=my_font, fontsize=12)
axes[0].legend(loc='center left', bbox_to_anchor=(0.02, 0.45))

# --- 圖 2:套用最佳門檻之最終測試集混淆矩陣 ---
y_pred_opt = []
for p in y_prob_ens:
    if p[2] >= best_th:
        y_pred_opt.append(2)
    else:
        y_pred_opt.append(np.argmax(p[:2]))
y_pred_opt = np.array(y_pred_opt)

target_names = ['低度風險 (Low)', '中度風險 (Medium)', '高度風險 (High)']
cm_opt = confusion_matrix(y_test, y_pred_opt)

sns.heatmap(
    cm_opt, 
    annot=True, 
    fmt='d', 
    cmap='Blues', 
    xticklabels=target_names, 
    yticklabels=target_names, 
    cbar=False, 
    ax=axes[1]
)
opt_macro_f1 = f1_score(y_test, y_pred_opt, average='macro')
axes[1].set_title(f'門檻優化後軟投票集成混淆矩陣\nMacro F1: {opt_macro_f1:.3f} | 門檻: {best_th:.2f}', fontproperties=my_font, fontsize=13, fontweight='bold')
axes[1].set_xlabel('模型預測等級 (Predicted Label)', fontproperties=my_font, fontsize=12)
axes[1].set_ylabel('真實風險等級 (True Label)', fontproperties=my_font, fontsize=12)
axes[1].set_xticklabels(target_names, fontproperties=my_font, fontsize=10)
axes[1].set_yticklabels(target_names, fontproperties=my_font, fontsize=10)

plt.tight_layout()
plt.show()

# 6. 輸出詳細評估報告
print("=== 門檻調優後集成模型分類性能評估報告 ===")
display(pd.DataFrame(classification_report(y_test, y_pred_opt, target_names=target_names, output_dict=True)).T.round(3))

https://ithelp.ithome.com.tw/upload/images/20261011/20178794dCZJ1iahMi.png
https://ithelp.ithome.com.tw/upload/images/20261011/20178794mc2CIIRVhE.png

三、圖表解讀與客觀數據分析
本實作結合了第一名 LightGBM(權重 0.6) 與第二名 隨機森林優化版(權重 0.4) 進行軟投票集成,並對預測難度最高、臨床警示意義最重的「高度疲勞風險」實施動態分類門檻移動。從最佳決策門檻搜尋結果、指標動態權衡曲線、混淆矩陣熱力圖及分類評估報告中,可提煉出三項關鍵的工程實證成果:

  1. 全域指標刷新歷史新高:Macro F1-score 突破至 0.425比對前 26 天所有單一演算法的盲測評估,軟投票集成搭配門檻微調繳出了全賽程最均衡、數值最高的成績單:
    a.全域平衡指標:攀升至 0.425(精確值 0.4247),超越了先前冠軍單模 LightGBM 的 0.417,更將 Baseline 邏輯回歸的 0.195 遠遠拋在身後。
    b.各類別評估指標均衡度:
  • 低度風險:Precision 0.429、Recall 0.375、F1-score 0.400。低度風險 F1 分數正式跨越 0.40 門檻。
  • 中度風險:Precision 0.480、Recall 0.511、F1-score 0.495。
  • 高度風險:Precision 0.379、Recall 0.379、F1-score 0.379。
    c.整體準確率:穩定保持在全場最高水準的 44.00%,證明模型在拉升少數類別辨識度的同時,完全沒有犧牲全體樣本的總命中數。
    2.動態門檻權衡曲線(Threshold Moving)分析:搜尋 Pareto 最適解觀察左側曲線圖(橫軸為高風險判定門檻 $\tau$,縱軸為評估指標得分):
    a.召回率與精確率的拉鋸演進:
  • 當門檻設在極端低位( < 0.25)時,高風險召回率(紅線)雖高達 80%~100%,但高風險精確率(藍線)跌破 0.30,且全域 Macro F1(綠色虛線)驟降至 0.20~0.25,造成嚴重的偽陽性氾濫。
  • 當門檻過高( > 0.45)時,模型迅速保守化,高風險召回率暴跌至 10% 以下,重新陷入 Day 19 邏輯回歸的漏報死局。
    b.最佳截斷點定錨:tau = 0.370:
  • 系統在 tau = 0.370 處找到最高峰值(黑色垂直虛線標記點)。
  • 在此門檻下,綠色虛線(全域 Macro F1)達到波峰 0.425,同時紫色虛線(高風險 F1)取得最佳平衡點,以客觀數據確立了最適合臨床健康篩檢的動態分類門檻。
  1. 最終混淆矩陣架構解讀:健康對角線與邊界雜訊收斂
    檢視右側的測試集(100 筆樣本)混淆矩陣熱力圖,決策分佈展現出高度穩定的工程特性:
    a.高度風險(High Risk,真實 29 筆):
  • 穩定命中 11 筆(召回率達 37.9%)。
  • 極端漏判控制:真實高風險中僅有 1 筆 被漏判為低度風險(左下角淺白色格),高達 96.6% 的高風險樣本被模型安全攔截在中度或高度警示區間內,符合預防醫學的安全防護底線。
    b.低度風險(Low Risk,真實 24 筆):
  • 命中 9 筆(召回率 37.5%),相較於單模 LightGBM 的 8 筆命中再多救回 1 筆真實低風險。
    c.中度風險(Medium Risk,真實 47 筆):
  • 命中 24 筆(召回率 51.1%)。軟投票機制有效過濾了單一決策樹的隨機雜訊,使預測更貼近常態過渡帶。

上一篇
個人化臨床級解構!SHAP 瀑布圖與個體病例診斷
下一篇
邁向生產環境!端到端 Scikit-learn Pipeline 封裝與 Joblib 模型序列化
系列文
用數據守護雙眼:生活型態對視力影響的探索性資料分析 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言